/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { ts, MethodSubSignature, MethodSignature, ArkPtrInvokeExpr, FieldSignature, ArkMethod, ArkInstanceFieldRef, ArkInstanceInvokeExpr, Local, ArkInvokeStmt, Stmt, FunctionType, NumberType, UnknownType, ArkAssignStmt, ArkStaticFieldRef, ArkStaticInvokeExpr, ClassSignature, ArkNormalBinopExpr, ArkNewExpr } from "arkanalyzer";
import { MatcherCallback, MatcherTypes, MethodMatcher } from "../../matcher/Matchers";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { Scene } from 'arkanalyzer';

const G_META_DATA: BaseMetaData = {
  severity: 2,
  ruleDocPath: "docs/no-implied-eval-check.md",
  description: "Implied eval. Consider passing a function.",
};
const FUNCTION_CONSTRUCTOR = 'Function';
const GLOBAL_CANDIDATES = new Set(['global', 'window', 'globalThis']);
const EVAL_LIKE_METHODS = new Set(['setImmediate', 'setInterval', 'setTimeout', 'execScript']);

export class NoImpliedEvalCheck implements BaseChecker {
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  readonly metaData: BaseMetaData = G_META_DATA;
  public rule: Rule;
  private buildMatcher: MethodMatcher = {
    matcherType: MatcherTypes.METHOD,
  };

  public registerMatchers(): MatcherCallback[] {
    const matchBuildCallback: MatcherCallback = {
      matcher: this.buildMatcher,
      callback: this.check
    };
    return [matchBuildCallback];
  }

  public check = (arkMethod: ArkMethod) => {
    const scene = arkMethod.getDeclaringArkFile().getScene();
    const statements = arkMethod.getBody()?.getCfg().getStmts() ?? [];
    for (const statement of statements) {
      if (statement instanceof ArkInvokeStmt) {
        const originalText = statement.getOriginalText();
        if (!originalText) continue;
        const filteredEvalLikeMethods = this.filterEvalLikeMethods(statement);
        const hasEvalLikeMethod = this.hasEvalLikeMethod(originalText, filteredEvalLikeMethods);
        const invokeExpression = statement.getInvokeExpr();
        if (hasEvalLikeMethod) {
          if (this.shouldSkipInvokeExpression(invokeExpression)) continue;
          const argumentsList = invokeExpression.getArgs();
          if (this.hasInvalidArgsTypes(argumentsList, scene, originalText)) {
            let regex = /(?:setTimeout|setInterval|setImmediate|execScript)\((.*?)(?:,|\))/;
            const match = originalText.match(regex);
            let text = match ? match[1].trim() : originalText;
            const isGlobalCandidate = this.hasGlobalCandidate(originalText);
            if (isGlobalCandidate) {
              text = this.getValueAst(originalText);
            }
            const finalText = text !== undefined ? text : originalText;
            this.addIssueReport(statement, finalText, this.metaData.description);
          }
        } else if (originalText.includes(FUNCTION_CONSTRUCTOR)) {
          if (invokeExpression instanceof ArkInstanceInvokeExpr) {
            const baseType = invokeExpression.getBase()?.getType()?.getTypeString();
            if (baseType && this.isFunctionConstructor(baseType)) {
              const base = invokeExpression.getBase();
              if (base && base instanceof Local) {
                const baseDeclaringStatement = base.getDeclaringStmt();
                if (baseDeclaringStatement && baseDeclaringStatement instanceof ArkAssignStmt) {
                  const rightOperand = baseDeclaringStatement.getRightOp();
                  if (rightOperand && rightOperand instanceof ArkNewExpr) {
                    const errorMessage = "Implied eval. Do not use the Function constructor to create functions.";
                    this.addIssueReport(statement, "new", errorMessage);
                  }
                }
              }
            }
          }
        }
      } else if (statement instanceof ArkAssignStmt) {
        const originalText = statement.getOriginalText();
        if (!originalText) continue;
        if (originalText.includes(FUNCTION_CONSTRUCTOR)) {
          const errorMessage = "Implied eval. Do not use the Function constructor to create functions.";
          const rightValue = statement.getRightOp();
          if (rightValue instanceof ArkInstanceInvokeExpr) {
            const base = rightValue?.getBase();
            if (base instanceof Local) {
              const baseType = rightValue.getBase().getType();
              if (baseType instanceof FunctionType || baseType instanceof UnknownType) {
                this.addIssueReport(statement, base.getName(), errorMessage);
              }
            }
          } else if (rightValue instanceof ArkStaticInvokeExpr) {
            const methodName = rightValue.getMethodSignature()?.getMethodSubSignature()?.getMethodName();
            if (methodName) { 
                this.addIssueReport(statement, methodName, errorMessage);
            }
          }
        }
      }
    }
  }

  private filterEvalLikeMethods(statement: ArkInvokeStmt): Set<string> {
    const methods = new Set(EVAL_LIKE_METHODS);
    const file = statement.getCfg().getDeclaringMethod().getDeclaringArkClass().getDeclaringArkFile();
    file.getImportInfos().forEach(importInfo => {
      const importName = importInfo.getImportClauseName();
      if (methods.has(importName)) {
        methods.delete(importName);
      }
    });
    const classMethods = statement.getCfg().getDeclaringMethod().getDeclaringArkClass().getMethods();
    classMethods.forEach(method => {
      const methodName = method.getName();
      if (methods.has(methodName)) {
        methods.delete(methodName);
      }
    });
    return methods;
  }

  private hasGlobalCandidate(originalText: string): boolean {
    return Array.from(GLOBAL_CANDIDATES).some(method => originalText.includes(method));
  }

  private hasEvalLikeMethod(originalText: string, methods: Set<string>): boolean {
    return Array.from(methods).some(method => originalText.includes(method));
  }

  private shouldSkipInvokeExpression(invokeExpression: any): boolean {
    if (invokeExpression instanceof ArkInstanceInvokeExpr) {
      const base = invokeExpression.getBase();
      if (base instanceof Local) {
        const name = base.getName();
        const hasGlobalCandidate = Array.from(GLOBAL_CANDIDATES).some(method => name === method);
        return !hasGlobalCandidate;
      }
    }
    return false;
  }

  private hasInvalidArgsTypes(argumentsList: any[], scene: Scene, originalText: string): boolean {
    const argsMax = argumentsList.length;
    const leftType = argumentsList[0]?.getType();
    const rightType = argumentsList[1]?.getType();

    if (argsMax === 1 ? !(leftType instanceof FunctionType) :
      !(leftType instanceof FunctionType) || !(rightType instanceof NumberType)) {
      const leftTypeValue = leftType?.getTypeString();
      if (leftTypeValue === 'Function') return false;
      if (argumentsList[0] instanceof Local) {
        const leftValue = argumentsList[0];
        const declaringStatement = leftValue.getDeclaringStmt();
        if (declaringStatement instanceof ArkAssignStmt) {
          const declaringRightValue = declaringStatement.getRightOp();
          if (declaringRightValue instanceof ArkNormalBinopExpr) {
            const rightOp1 = declaringRightValue.getOp1();
            const rightOp2 = declaringRightValue.getOp2();
            if (rightOp1 && rightOp2) {
              const type1 = rightOp1.getType();
              const type2 = rightOp2.getType();
              if (type1 instanceof FunctionType && type2 instanceof FunctionType) {
                return false;
              }
            }
          }
        }
      }

      if (leftType instanceof UnknownType) {
        if (argumentsList[0] instanceof Local) {
          const leftValue = argumentsList[0];
          const declaringStatement = leftValue.getDeclaringStmt();
          if (declaringStatement instanceof ArkAssignStmt) {
            const declaringRightValue = declaringStatement.getRightOp();
            if (declaringRightValue instanceof ArkStaticFieldRef) {
              const declaringRightType = declaringRightValue.getFieldSignature().getType();
              if (declaringRightType instanceof FunctionType) {
                return false;
              }
            } else if (declaringRightValue instanceof ArkInstanceInvokeExpr) {
              const base = declaringRightValue.getBase();
              if (base instanceof Local) {
                const type = base.getType();
                if (type instanceof FunctionType) {
                  return false;
                }
              }
            } else if (declaringRightValue instanceof ArkInstanceFieldRef) {
              let isFunction = false;
              const mFieldSignature = declaringRightValue.getFieldSignature();
              if (mFieldSignature instanceof FieldSignature) {
                const getDeclaringSignature = mFieldSignature.getDeclaringSignature();
                if (getDeclaringSignature instanceof ClassSignature) {
                  const mClass = scene.getClass(getDeclaringSignature);
                  const mFields = mClass?.getFields();
                  if (mFields && mFields.length > 0) {
                    for (const field of mFields) {
                      const initializer = field.getInitializer();
                      for (const minitializer of initializer) {
                        if (minitializer instanceof ArkAssignStmt) {
                          const initializerRightValue = minitializer.getRightOp();
                          const type = initializerRightValue.getType();
                          if (type instanceof FunctionType) {
                            isFunction = true;
                            break;
                          }
                        }
                      }
                      if (isFunction) {
                        break;
                      }
                    }
                  } else {
                    const base = declaringRightValue.getBase();
                    if (base) {
                      if (base instanceof Local) {
                        const baseDeclaringStatement = base.getDeclaringStmt();
                        if (baseDeclaringStatement instanceof ArkAssignStmt) {
                          const baseDeclaringStmtText = baseDeclaringStatement.getOriginalText();
                          if (baseDeclaringStmtText) {
                            const isFunctionType = this.checkTypeAst(baseDeclaringStmtText, originalText);
                            if (isFunctionType) {
                              return false;
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
              if (isFunction) {
                return false;
              }
            } else if (declaringRightValue instanceof ArkPtrInvokeExpr) {
              const funcPtrLocal = declaringRightValue.getFuncPtrLocal();
              if (funcPtrLocal instanceof Local) {
                const type = funcPtrLocal.getType();
                if (type instanceof FunctionType) {
                  const methodSignature = type.getMethodSignature();
                  if (methodSignature instanceof MethodSignature) {
                    const getMethodSubSignature = methodSignature.getMethodSubSignature();
                    if (getMethodSubSignature instanceof MethodSubSignature) {
                      const returnType = getMethodSubSignature.getReturnType();
                      if (returnType instanceof FunctionType) {
                        return false;
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
      return true;
    }
    return false;
  }

  private isFunctionConstructor(baseType: string): boolean {
    return baseType.includes('Function') || baseType.includes('window.Function') || baseType.includes('window[\'Function\']');
  }

  private checkTypeAst(baseDeclaringStmtText: string, originalText: string): boolean {
    const ast = ts.createSourceFile('temp.ts', baseDeclaringStmtText, ts.ScriptTarget.ESNext, true);
    const visit = (node: ts.Node): boolean => {
      if (ts.isMethodDeclaration(node)) {
        if (ts.isIdentifier(node.name)) {
          const name = node.name.text;
          if (originalText.includes(name)) {
            return true;
          }
        }
      }
      return ts.forEachChild(node, visit) || false;
    };
    return visit(ast);
  }

  private getValueAst(originalText: string): string {
    const ast = ts.createSourceFile('temp.ts', originalText, ts.ScriptTarget.ESNext, true);
    const visit = (node: ts.Node): string => {
      if (ts.isCallExpression(node)) {
        if (node.arguments.length > 0) {
          const text = node.arguments[0].getText();
          return text;
        }
      }
      const result = ts.forEachChild(node, visit);
      return result || originalText;
    };
    return visit(ast);
  }

  private addIssueReport(statement: Stmt, name: string, errorMessage: string) {
    const warnInfo = this.getLineAndColumn(statement, name);
    const defect = (new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol,errorMessage, this.metaData.severity, this.rule.ruleId,
      warnInfo.filePath, this.metaData.ruleDocPath, true, false, false))
    this.issues.push(new IssueReport(defect, undefined));
    RuleListUtil.push(defect);
  }

  private getLineAndColumn(statement: Stmt, name: string) {
    const arkFile = statement.getCfg()?.getDeclaringMethod().getDeclaringArkClass().getDeclaringArkFile();
    if (arkFile) {
      const originText = statement.getOriginalText() ?? '';
      const pos = originText.indexOf(name);
      if (pos !== -1) {
        const originPosition = statement.getOriginPositionInfo();
        const line = originPosition.getLineNo();
        let startCol = originPosition.getColNo();
        startCol += pos;
        const endCol = startCol + name.length - 1;
        const originPath = arkFile.getFilePath();
        return { line, startCol, endCol, filePath: originPath };
      }
    }
    return { line: -1, startCol: -1, endCol: -1, filePath: '' };
  }
}